package org.dromara.soul.web.plugin.custom;

import org.apache.commons.lang.StringUtils;
import org.dromara.soul.common.constant.Constants;
import org.dromara.soul.common.dto.RuleData;
import org.dromara.soul.common.dto.SelectorData;
import org.dromara.soul.common.dto.convert.custom.AuthHandler;
import org.dromara.soul.common.enums.PluginEnum;
import org.dromara.soul.common.enums.PluginTypeEnum;
import org.dromara.soul.common.exception.CommonErrorCode;
import org.dromara.soul.common.utils.GsonUtils;
import org.dromara.soul.common.utils.LogUtils;
import org.dromara.soul.web.cache.LocalCacheManager;
import org.dromara.soul.web.plugin.AbstractSoulPlugin;
import org.dromara.soul.web.plugin.SoulPluginChain;
import org.dromara.soul.web.request.CustomServerHttpRequestDecorator;
import org.dromara.soul.web.request.CustomServerWebExchangeDecorator;
import org.dromara.soul.web.request.RequestDTO;
import org.dromara.soul.web.result.BaseResult;
import org.dromara.soul.web.result.SoulResultEnum;
import org.dromara.soul.web.result.SoulResultUtils;
import org.dromara.soul.web.result.SoulResultWarp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.time.Duration;
import java.util.HashMap;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

/**
* @Description
* @Author hex
* @CreateDate 2020/1/19 9:47
*/
public class AuthPlugin extends AbstractSoulPlugin {

	private final LoadBalancerClient loadBalancer;

	private final WebClient webClient;

	/**
	 * logger.
	 */
	private static final Logger LOGGER = LoggerFactory.getLogger(AuthPlugin.class);

	public AuthPlugin(LocalCacheManager localCacheManager, final LoadBalancerClient loadBalancer, WebClient webClient) {
		super(localCacheManager);
		this.loadBalancer = loadBalancer;
		this.webClient = webClient;
	}

	/**
	 * this is Template Method child has Implement your own logic.
	 *
	 * @param exchange exchange the current server exchange {@linkplain ServerWebExchange}
	 * @param chain    chain the current chain  {@linkplain ServerWebExchange}
	 * @param selector selector    {@linkplain SelectorData}
	 * @param rule     rule    {@linkplain RuleData}
	 * @return {@code Mono<Void>} to indicate when request handling is complete
	 */
	@Override
	protected Mono<Void> doExecute(ServerWebExchange exchange, SoulPluginChain chain, SelectorData selector, RuleData rule) {
		// 从exchange获取requestDTO
		final RequestDTO requestDTO = exchange.getAttribute(Constants.REQUESTDTO);
		assert requestDTO != null;
		// 获取请求头
		HttpHeaders headers = exchange.getRequest().getHeaders();
		// 获取auth规则
		final AuthHandler ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), AuthHandler.class);
		// 带项目前缀的路径 防止不同项目之间存在相同的url,但是鉴权不同
		String path = requestDTO.getPath();
		LOGGER.info("path is :{}", path);
		// 获取忽略路径
		Set<String> ignorePathSet = ruleHandle.getIgnorePathSet();
		// 真实路径在忽略列表则执行下一链路
		if(ignorePathSet.contains(path)){
			return chain.execute(exchange);
		}
		String whitePath = "false";
		// 获取白名单路径
		Set<String> whitePathSet = ruleHandle.getWhitePathSet();
		if(whitePathSet.contains(path)){
			whitePath = "true";
		}
		// 获取平台标志
		String platformTag = headers.getFirst("platformTag");
		// 获取配置的平台标志
		Set<String> platformTagSet = ruleHandle.getPlatformTagSet();
		// 输入的平台标志不在配置里则抛出异常
		if(!platformTagSet.contains(platformTag)){
			Object error = SoulResultWarp.error(SoulResultEnum.PLATFORM_TAG_IS_ERROR.getCode(), SoulResultEnum.PLATFORM_TAG_IS_ERROR.getMsg(), null);
			return SoulResultUtils.result(exchange, error);
		}
		// 获取服务名
		String serverId = ruleHandle.getServerId();

		if (StringUtils.isBlank(serverId) || StringUtils.isBlank(ruleHandle.getPath()) || StringUtils.isBlank(ruleHandle.getPostApplicationJsonPath())) {
			Object error = SoulResultWarp.error(SoulResultEnum.CANNOT_CONFIG_AUTH_SERVICEID.getCode(), SoulResultEnum.CANNOT_CONFIG_AUTH_SERVICEID.getMsg(), null);
			return SoulResultUtils.result(exchange, error);
		}
		// 使用loadBalancer通过服务名找到服务实例
		final ServiceInstance serviceInstance = loadBalancer.choose(serverId);
		if (Objects.isNull(serviceInstance)) {
			Object error = SoulResultWarp.error(SoulResultEnum.AUTH_SERVICEID_IS_ERROR.getCode(), SoulResultEnum.AUTH_SERVICEID_IS_ERROR.getMsg(), null);
			return SoulResultUtils.result(exchange, error);
		}

		// 获取地址
		final URI uri;
		// 获取方法
		String httpMethod = requestDTO.getHttpMethod();
		// 获取类型
		String contentType = Optional.ofNullable(exchange
				.getRequest()
				.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE))
				.orElse(MediaType.APPLICATION_JSON_UTF8_VALUE);
		// get方法?
		boolean getMethod = httpMethod.equals(HttpMethod.GET.name()) ? true : false;
		// 是否包含application/json
		boolean applicationJson = contentType.contains(MediaType.APPLICATION_JSON_VALUE) ? true : false;
		// post请求且application/json或者application/json;charset=UTF-8则走PostApplicationJsonPath
		if (!getMethod && applicationJson) {
			uri = loadBalancer.reconstructURI(serviceInstance, URI.create(ruleHandle.getPostApplicationJsonPath()));
		}
		// get或者非application/json走path
		else{
			uri = loadBalancer.reconstructURI(serviceInstance, URI.create(ruleHandle.getPath()));
		}
		// 获取uri地址
		String urlPath = uri.toASCIIString();
		// get请求组装请求参数
		if(getMethod){
			String query = exchange.getRequest().getURI().getQuery();
			if (StringUtils.isNotBlank(query)) {
				urlPath = urlPath + "?" + query;
			}
		}

		long timeout = ruleHandle.getTimeout();
		LOGGER.info("you request,The resulting urlPath is :{}", urlPath);
		// 自定义ServerHttpRequest
		CustomServerHttpRequestDecorator customServerHttpRequestDecorator = new CustomServerHttpRequestDecorator(exchange.getRequest());
		// 自定义ServerWebExchange
		CustomServerWebExchangeDecorator customServerWebExchangeDecorator = new CustomServerWebExchangeDecorator(exchange, customServerHttpRequestDecorator);
		// .method发送什么请求  .url地址路径
		HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
		LOGGER.info("The real_ur is :{}", requestDTO.getRealUrl());
		String finalWhitePath = whitePath;
		LOGGER.info("finalWhitePath is :{}", finalWhitePath);
		return webClient.method(method).uri(urlPath).
				headers(httpHeaders -> {
					httpHeaders.addAll(headers);
					httpHeaders.add(Constants.AUTH_REAL_URL,requestDTO.getRealUrl());
					httpHeaders.add(Constants.WHITE_PATH, finalWhitePath);
					httpHeaders.remove(HttpHeaders.HOST);
				})
				.contentType(MediaType.valueOf(contentType))
				.body(BodyInserters.fromDataBuffers(customServerWebExchangeDecorator.getRequest().getBody()))
				.exchange()
				.doOnError(e -> LogUtils.error(LOGGER, e::getMessage))
				.timeout(Duration.ofMillis(timeout))
				.flatMap(response -> doNext(response, customServerWebExchangeDecorator, chain));
		// 通过feign或者loadBalance调用权限认证模块获取用户信息及权限 失败或者权限不足返回 通过继续执行逻辑
	}

	private Mono<Void> doNext(final ClientResponse res, final ServerWebExchange exchange, final SoulPluginChain chain) {
		if (res.statusCode().is2xxSuccessful()) {
			Mono<BaseResult> baseResultMono = res.bodyToMono(BaseResult.class);
			Mono<Void> mono = baseResultMono.flatMap(baseResult ->{
				if(CommonErrorCode.SUCCESSFUL == baseResult.getCode()){
					HashMap<String,String> map = (HashMap<String,String>)baseResult.getData();
					LOGGER.info("auth response map is [{}]", map.toString());
					String userNo = map.get(Constants.USER_NO);
					String white = StringUtils.isBlank(map.get("white")) ? "false" : map.get("white");
					LOGGER.info("auth response userNo is [{}], white is [{}]", userNo, white);
					exchange.getAttributes().put(Constants.USER_NO, userNo);
					exchange.getAttributes().put(Constants.WHITE, white);
					return chain.execute(exchange);
				} else{
					Object error = SoulResultWarp.error(baseResult.getCode(), baseResult.getMessage(), null);
					return SoulResultUtils.result(exchange, error);
				}
			});
			return mono;
		} else {
			Object error = SoulResultWarp.error(SoulResultEnum.AUTH_SERVICE_RESPONSE_ERROR.getCode(), SoulResultEnum.AUTH_SERVICE_RESPONSE_ERROR.getMsg(), null);
			return SoulResultUtils.result(exchange, error);
		}
	}

	/**
	 * return plugin type.
	 * the plugin execution order
	 * before type The first to perform then Function Type ,then last type.
	 *
	 * @return {@linkplain PluginTypeEnum}
	 */
	@Override
	public PluginTypeEnum pluginType() {
		return PluginTypeEnum.FUNCTION;
	}

	/**
	 * return plugin order .
	 * This attribute To determine the plugin execution order in the same type plugin.
	 *
	 * @return int order
	 */
	@Override
	public int getOrder() {
		return PluginEnum.AUTH.getCode();
	}

	/**
	 * acquire plugin name.
	 * this is plugin name define  if you extend {@linkplain AbstractSoulPlugin } you must Provide the right name.
	 * if you impl AbstractSoulPlugin this attribute not use.
	 *
	 * @return plugin name.
	 */
	@Override
	public String named() {
		return PluginEnum.AUTH.getName();
	}
}
